/*
 * Copyright (c) 2001-2007, Inversoft, All Rights Reserved
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific
 * language governing permissions and limitations under the License.
 */
package org.inversoft.vertigo.database;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Map;
import java.util.HashMap;
import java.util.logging.Logger;

import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.EntityManager;

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

/**
 * <p>
 * This class is used to create the database and all of the tables for
 * the application. The database is created via Hibernates create mode
 * for the hibernate.hbm2ddl.auto property. After the database has been
 * created it is seeded with data by executing a series of seed SQL scripts
 * that are contained in a specific directory.
 * </p>
 *
 * <p>
 * In order to correctly handling seeding, this class calls the
 * {@link org.inversoft.vertigo.database.DatabasePatcher} class passing it the seed directory. That
 * class knows how to load and execute the files in numerical order
 * based on the file names.
 * </p>
 *
 * @author  Brian Pontarelli
 */
public class DatabaseLoader {
    private static final Logger logger = Logger.getLogger(DatabaseLoader.class.getName());
    private String persistenceUnit;
    private Connection connection;
    private String projectName;
    private boolean projectContainsDomain;
    private File createSQLDir;
    private File patchSQLDir;
    private File seedSQLDir;

    /**
     * This main method provides an external execution point to the DatabaseLoader
     * Typically this is executed via a script; (ant, shell, etc)
     *
     * @param   args 1 - persistence unit, 2 - jdbc-connection-url, 3 - project name
     * @throws  SQLException on sql exception
     * @throws  IOException on io exception
     */
    public static void main(String... args) throws SQLException, IOException {
        if (args.length != 5) {
            System.err.println("Invalid arguments " + Arrays.asList(args));
            System.err.println("Usage: DatabaseLoader <persistence-unit> <db-url> <project-name> <project-contains-domain-boolean> <root-dir>");
            System.exit(1);
        }

        String persistenceUnit = args[0];
        String dbURL = args[1];
        String projectName = args[2];
        boolean projectContainsDomain = Boolean.parseBoolean(args[3]);
        String rootDir = args[4];

        MockJNDI jndi = new MockJNDI();
        MysqlDataSource dataSource = DatabaseTools.setupJDBCURLandJNDI(jndi, dbURL, projectName);
        jndi.activate();

        // Using the SQL run system we need to load up the database instance and then the tables
        // and any modifications to date in order. This works based on version numbers (1, 2, ...)
        // So we just force it by setting the last version to zero
        DatabaseLoader loader = new DatabaseLoader(persistenceUnit, dataSource.getConnection(), projectName,
            projectContainsDomain, new File(rootDir, "create"), new File(rootDir, "patch"),
            new File(rootDir, "seed"));
        loader.load();
    }

    /**
     * Creates a new database loader that will use the persistence unit name for loading up the database
     * and tables via Hibernates create mode. After that this class will use the JDBC connection, seed
     * SQL directory and last version number to seed the database with data from the seed files.
     *
     * @param   persistenceUnit The JPA persistence unit to use with Hibernate's create mode.
     * @param   connection The JDBC connection to use when seeding the database.
     * @param   projectName THe name of this project for patching.
     * @param   projectContainsDomain True if this project contains any domain objects. False if it
     *          doesn't. This is used to determine if hibernate should be used to create the tables.
     *          If the project doesn't have any domain objects than we must assume that all the domain
     *          objects come from the classpath. If this is true, than we can check to see if there
     *          are create scripts in the path.
     * @param   createSQLDir The directory on the file system that contains the create SQL files.
     * @param   patchSQLDir The directory on the file system that contains the patch SQL files.
     * @param   seedSQLDir The directory on the file system that contains the seed files.
     * @throws  IllegalArgumentException If the seed SQL directory is not valid.
     */
    public DatabaseLoader(String persistenceUnit, Connection connection, String projectName,
        boolean projectContainsDomain, File createSQLDir, File patchSQLDir, File seedSQLDir) {
        this.persistenceUnit = persistenceUnit;
        this.connection = connection;
        this.projectName = projectName;
        this.projectContainsDomain = projectContainsDomain;
        this.createSQLDir = createSQLDir;
        this.patchSQLDir = patchSQLDir;
        this.seedSQLDir = seedSQLDir;
    }

    /**
     * Calls Hibernate to create the database tables and then runs the seed files using the
     * {@link DatabasePatcher}.
     *
     * @throws  IOException If anything goes wrong while seeding.
     * @throws  SQLException If any of the SQL scripts or versions table control failed.
     */
    public void load() throws IOException, SQLException {
        // Disable keys so that we can load in any order.
        DatabaseTools.disableKeys(connection);

        boolean created = false;
        try {
            // Determine if there are create scripts for this project or any included modules
            File create = new File(createSQLDir, "tables.sql");
            if (create.exists()) {
                logger.info("Running local create script from [" + create + "]");
                ScriptExecutor executor = new ScriptExecutor(connection);
                executor.execute(create);
                created = true;

                // Next load all the JAR files (components/libraries)
                createTablesClasspath();
            } else {
                // Check the class path for create scripts
                if (!projectContainsDomain) {
                    Enumeration<URL> urls = this.getClass().getClassLoader().getResources("META-INF/sql/create/tables.sql");
                    if (urls.hasMoreElements()) {
                        createTablesClasspath();
                        created = true;
                    }
                }

                if (!created) {
                    // Use hibernate
                    logger.info("Using Hibernate to create tables [" + createSQLDir + "]");
                    Map<String, String> params = new HashMap<String, String>();
                    params.put("hibernate.hbm2ddl.auto", "create");
                    EntityManagerFactory emf = Persistence.createEntityManagerFactory(persistenceUnit, params);
                    EntityManager em = emf.createEntityManager();
                    em.close();
                    emf.close();
                }
            }
        } finally {
            // Finally enable the keys again
            DatabaseTools.enableKeys(connection);
        }

        // Now run all the local and classpath patches. This will also seed things as it goes.
        // If the create directory exists, we didn't use hibernate to create the tables, we
        // should also run any patch scripts.
        DatabasePatcher patcher = new DatabasePatcher(connection, projectName, patchSQLDir, seedSQLDir, !created);
        patcher.patch();
    }

    /**
     * Executes all the scripts in the classpath.
     *
     * @throws  IOException If the entries in the classpath couldn't be loaded or executed.
     */
    protected void createTablesClasspath() throws IOException {
        ScriptExecutor executor = new ScriptExecutor(connection);
        Enumeration<URL> urls = this.getClass().getClassLoader().getResources("META-INF/sql/create/tables.sql");
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            logger.info("Running classpath create script from [" + url + "]");
            executor.execute(url);
        }
    }
}